package net.shambolica.ember.jinterface.wrappers.process;

import com.ericsson.otp.erlang.*;
import net.shambolica.ember.jinterface.wrappers.objects.EOMatch;
import net.shambolica.ember.jinterface.wrappers.objects.EOMatchSpec;

import static java.lang.Math.max;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedList;

/**
 * Wrapper for process - generally used for send/receive functionality.
 * Only one thread is meant to use a process wrapper at a time.
 * However, all access is synchronized to allow polling from multiple
 * threads.
 */
public class OtpProcessWrapper {

    private final OtpMbox mbox;
    private final LinkedList<OtpErlangObject> buffer;

    public OtpProcessWrapper(OtpMbox mbox) {
        this.mbox = mbox;
        this.buffer = new LinkedList<OtpErlangObject>();
    }

    public OtpProcessWrapper(OtpNode node) {
        this.mbox = node.createMbox();
        this.buffer = new LinkedList<OtpErlangObject>();
    }

    public OtpProcessWrapper(OtpNode node, String registeredName) {
        this.mbox = node.createMbox(registeredName);
        this.buffer = new LinkedList<OtpErlangObject>();
    }


    public OtpErlangPid self() {
        return mbox.self();
    }

    public String getNode() {
        return self().node();
    }


    public Collection<String> getRegisteredNames() {
        return Arrays.asList(mbox.getNames());
    }


    public synchronized void send(OtpErlangPid toPid, OtpErlangObject message) {
        mbox.send(toPid, message);
    }


    public synchronized void send(String toRegisteredName, OtpErlangObject message) {
        mbox.send(toRegisteredName, message);
    }


    public synchronized void send(String atNode, String registeredName, OtpErlangObject message) {
        mbox.send(atNode, registeredName, message);
    }


    /**
     * Wait indefinitely to receive a message.
     * @return                          The message object.
     * @throws OtpErlangExit            On exit.
     * @throws OtpErlangDecodeException On decode error.
     */
    public synchronized OtpErlangObject receive() throws OtpErlangExit, OtpErlangDecodeException {
        if (buffer.isEmpty()) {
            return mbox.receive();
        } else {
            return buffer.getFirst();
        }
    }


    /**
     * Wait for timeout milliseconds to receive a message.
     * @param timeout                   Milliseconds to wait for a message.
     * @return                          Message received, or null if no message.
     * @throws OtpErlangExit            On exit.
     * @throws OtpErlangDecodeException On decode error.
     */
    public synchronized OtpErlangObject receive(long timeout) throws OtpErlangExit, OtpErlangDecodeException {
        if (buffer.isEmpty()) {
            return mbox.receive(timeout);
        } else {
            return buffer.getFirst();
        }
    }


    /**
     * Receive by matchspec.
     * @param matchSpec                 The matchspec to match on.
     * @return                          A match object, once a match has been made.
     * @throws OtpErlangExit            On exit.
     * @throws OtpErlangDecodeException On decode error.
     */
    public synchronized EOMatch receive(EOMatchSpec matchSpec) throws OtpErlangExit, OtpErlangDecodeException {
        // First, go for buffered list.
        EOMatch ret = null;
        for (OtpErlangObject it: buffer) {
            EOMatch match = matchSpec.match(it);
            if (match.isMatched()) {
                ret = match;
                break;
            }
        }

        if (ret != null) {
            buffer.remove(ret.getObject());
        } else {
            for (OtpErlangObject obj = mbox.receive();
                 !(ret = matchSpec.match(obj)).isMatched();
                 obj = mbox.receive()) {
                buffer.addLast(obj);
            }
        }

        return ret;
    }


    /**
     * Receive by matchspec, with timeout (in milliseconds).
     * Note that even with a timeout of 0 (or less), at least one receive poll will be made.
     * @param matchSpec                 The matchspec to match on.
     * @param timeout                   Milliseconds to wait for a message.
     * @return                          A match object if a match was made, or null if timed out.
     * @throws OtpErlangExit            On exit.
     * @throws OtpErlangDecodeException On decode error.
     */
    public synchronized EOMatch receive(EOMatchSpec matchSpec, long timeout) throws OtpErlangExit, OtpErlangDecodeException {
        if (timeout < 0) {
            timeout = 0;
        }
        long limit = System.currentTimeMillis() + timeout;
        
        // First, go for buffered list.
        EOMatch ret = null;
        for (OtpErlangObject it: buffer) {
            EOMatch match = matchSpec.match(it);
            if (match.isMatched()) {
                ret = match;
                break;
            }
        }

        if (ret != null) {
            buffer.remove(ret.getObject());
        } else {
            for (OtpErlangObject obj = mbox.receive(max(limit-System.currentTimeMillis(), 0));
                 obj != null && !(ret = matchSpec.match(obj)).isMatched() && limit - System.currentTimeMillis() > 0;
                 obj = mbox.receive(max(limit-System.currentTimeMillis(), 0))) {
                buffer.addLast(obj);
                ret = null;
            }
        }

        return ret;

    }

    public void sendExit(OtpErlangObject reason) {
        mbox.exit(reason);
    }
}
